--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 7377a39644074191b97b90483cdac5f2a96703f2
Parents : 5348370
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-17T23:27:09-05:00
feat(android): add CAMERA permission and handle incoming intents with custom URI schemes
Changes
2 files changed, 119 insertions(+), 30 deletions(-)
Diff
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ffefcd23..0ee6ecc7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
@@ -36,6 +37,24 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="lxma" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="lxmf" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="lxm" />
+ </intent-filter>
</activity>
</application>
diff --git a/android/app/src/main/java/com/meshchatx/MainActivity.java b/android/app/src/main/java/com/meshchatx/MainActivity.java
index ebdf8169..d08b76ce 100644
--- a/android/app/src/main/java/com/meshchatx/MainActivity.java
+++ b/android/app/src/main/java/com/meshchatx/MainActivity.java
@@ -18,6 +18,7 @@ import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
+import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
@@ -35,6 +36,7 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
+import org.json.JSONObject;
public class MainActivity extends AppCompatActivity {
private WebView webView;
@@ -45,7 +47,7 @@ public class MainActivity extends AppCompatActivity {
private static final String SERVER_URL = "https://127.0.0.1:8000";
private static final int SERVER_PORT = 8000;
private static final int RUNTIME_PERMISSIONS_REQUEST_CODE = 1001;
- private static final int MICROPHONE_WEB_PERMISSION_REQUEST_CODE = 1002;
+ private static final int WEB_MEDIA_PERMISSION_REQUEST_CODE = 1003;
private static final String PREFS_NAME = "meshchatx";
private static final String PREF_BATTERY_OPT_REQUESTED = "battery_opt_requested";
private static final int MAX_CONNECTION_ATTEMPTS = 120;
@@ -58,6 +60,7 @@ public class MainActivity extends AppCompatActivity {
private boolean startupPageLoaded = false;
private boolean backendFailed = false;
private int connectionAttempts = 0;
+ private String pendingIntentUri = null;
private static final String[] STARTUP_PHASES = new String[] {
"Starting MeshChatX...",
"Initializing Reticulum network stack...",
@@ -134,6 +137,7 @@ public class MainActivity extends AppCompatActivity {
progressBar.setVisibility(android.view.View.GONE);
loadingText.setVisibility(android.view.View.GONE);
errorText.setVisibility(android.view.View.GONE);
+ dispatchPendingIntentUri();
}
@Override
@@ -182,6 +186,8 @@ public class MainActivity extends AppCompatActivity {
handler.proceed();
}
});
+ webView.addJavascriptInterface(new MeshChatXAndroidBridge(this), "MeshChatXAndroid");
+
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onPermissionRequest(final PermissionRequest request) {
@@ -191,26 +197,46 @@ public class MainActivity extends AppCompatActivity {
}
boolean needsAudioCapture = false;
+ boolean needsVideoCapture = false;
for (String resource : request.getResources()) {
if (PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(resource)) {
needsAudioCapture = true;
- break;
+ } else if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource)) {
+ needsVideoCapture = true;
}
}
- if (!needsAudioCapture) {
+ if (!needsAudioCapture && !needsVideoCapture) {
request.grant(request.getResources());
return;
}
- if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
- == PackageManager.PERMISSION_GRANTED) {
+ List<String> missingPermissions = new ArrayList<>();
+ if (
+ needsAudioCapture &&
+ ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
+ != PackageManager.PERMISSION_GRANTED
+ ) {
+ missingPermissions.add(Manifest.permission.RECORD_AUDIO);
+ }
+ if (
+ needsVideoCapture &&
+ ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED
+ ) {
+ missingPermissions.add(Manifest.permission.CAMERA);
+ }
+ if (missingPermissions.isEmpty()) {
request.grant(request.getResources());
return;
}
pendingWebPermissionRequest = request;
- requestMicrophonePermissionForWebView();
+ ActivityCompat.requestPermissions(
+ MainActivity.this,
+ missingPermissions.toArray(new String[0]),
+ WEB_MEDIA_PERMISSION_REQUEST_CODE
+ );
});
}
@@ -255,11 +281,22 @@ public class MainActivity extends AppCompatActivity {
return true;
}
});
+ handleIncomingIntent(getIntent());
startMeshChatServer();
scheduleConnectionRetry("Connecting to local server...");
}
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ handleIncomingIntent(intent);
+ if (startupPageLoaded) {
+ dispatchPendingIntentUri();
+ }
+ }
+
private void requestRuntimePermissionsIfNeeded() {
List<String> missingPermissions = new ArrayList<>();
addIfMissing(missingPermissions, Manifest.permission.RECORD_AUDIO);
@@ -279,22 +316,6 @@ public class MainActivity extends AppCompatActivity {
}
}
- private void requestMicrophonePermissionForWebView() {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
- if (pendingWebPermissionRequest != null) {
- pendingWebPermissionRequest.grant(pendingWebPermissionRequest.getResources());
- pendingWebPermissionRequest = null;
- }
- return;
- }
-
- ActivityCompat.requestPermissions(
- this,
- new String[] { Manifest.permission.RECORD_AUDIO },
- MICROPHONE_WEB_PERMISSION_REQUEST_CODE
- );
- }
-
private void requestBatteryOptimizationExemptionIfNeeded() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
@@ -333,18 +354,23 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode == MICROPHONE_WEB_PERMISSION_REQUEST_CODE) {
- if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
- if (pendingWebPermissionRequest != null) {
- pendingWebPermissionRequest.deny();
- pendingWebPermissionRequest = null;
- }
+ if (requestCode == WEB_MEDIA_PERMISSION_REQUEST_CODE) {
+ if (pendingWebPermissionRequest == null) {
return;
}
- if (pendingWebPermissionRequest != null) {
+ boolean granted = true;
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ granted = false;
+ break;
+ }
+ }
+ if (granted) {
pendingWebPermissionRequest.grant(pendingWebPermissionRequest.getResources());
- pendingWebPermissionRequest = null;
+ } else {
+ pendingWebPermissionRequest.deny();
}
+ pendingWebPermissionRequest = null;
return;
}
if (requestCode != RUNTIME_PERMISSIONS_REQUEST_CODE) {
@@ -384,6 +410,34 @@ public class MainActivity extends AppCompatActivity {
return url != null && url.startsWith(SERVER_URL);
}
+ private void handleIncomingIntent(Intent intent) {
+ if (intent == null || !Intent.ACTION_VIEW.equals(intent.getAction())) {
+ return;
+ }
+ Uri data = intent.getData();
+ if (data == null || data.getScheme() == null) {
+ return;
+ }
+ String scheme = data.getScheme().toLowerCase();
+ if (!"lxma".equals(scheme) && !"lxmf".equals(scheme) && !"lxm".equals(scheme)) {
+ return;
+ }
+ pendingIntentUri = data.toString();
+ }
+
+ private void dispatchPendingIntentUri() {
+ if (webView == null || pendingIntentUri == null || pendingIntentUri.isEmpty()) {
+ return;
+ }
+ String uri = pendingIntentUri;
+ pendingIntentUri = null;
+ String js =
+ "window.dispatchEvent(new CustomEvent('meshchatx-intent-uri',{detail:" +
+ JSONObject.quote(uri) +
+ "}));";
+ webView.evaluateJavascript(js, null);
+ }
+
private void scheduleConnectionRetry(String message) {
if (startupPageLoaded || backendFailed) {
return;
@@ -484,5 +538,21 @@ public class MainActivity extends AppCompatActivity {
webView.destroy();
}
}
+
+ private static class MeshChatXAndroidBridge {
+ private final MainActivity activity;
+
+ MeshChatXAndroidBridge(MainActivity activity) {
+ this.activity = activity;
+ }
+
+ @JavascriptInterface
+ public void exitApp() {
+ activity.runOnUiThread(() -> {
+ activity.finishAffinity();
+ android.os.Process.killProcess(android.os.Process.myPid());
+ });
+ }
+ }
}
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────